#-------------------------------------------------------------------------------
# Copyright (c) 2012 Huseyin BIYIK.
# All rights reserved. This program and the accompanying materials
# are made available under the terms of the GNU Public License v3.0
# which accompanies this distribution, and is available at
# http://www.gnu.org/licenses/gpl.html
# 
# Contributors:
#     Huseyin BIYIK - initial API and implementation
#-------------------------------------------------------------------------------

#TODO LIST:

# 1. implement paket object type, do not use direct call manipulation with parsed packets
# 2. add probe & stun(t) packet types for symetric nat traversal
# 3. statically  define header & payload lengths do not use ascii , usee binary encoded data instead
# 4. implement generic api and define safe internal 6 externals
# 5. 1. buffering is impossible when  I/O buffer is full udp socket does not send proper packets, Python specific GIL issue
# 6. implement crc checks for basic flow control
# 7. implement sequence for each datagram for basic flow control
# 8. implement queue ordering and retransmissiom mechanism for basic flow control
# 9. add adressing standart for protocol

import socket, threading, random, uuid, time, logging, select
from messages import *
from call import *
from defs import logger
import networking
from streaming import buffer, stypes

##dispatcher to be multithreaded so it wont bother stack
class dispatcher(threading.Thread):
	def __init__(self, api, eventtype, args=None):
		threading.Thread.__init__(self)
		self.eventtype = eventtype
		self.args = args
		self.api = api
		self.start()
	
	def run(self):
		if self.eventtype == "datareceived":
##			self.buffer.add(args[1],args[2],self.api.outfunc)
			self.api._on_datareceived(self.args)
			return
		elif self.eventtype == "streamaccepted":
			return
		elif self.eventtype == "streamdeclined":
			return
		elif self.eventtype == "callestablished":
			self.api._on_call_established(self.args)
			return
		elif self.eventtype == "calldeclined":
			self.api._on_call_declined(self.args)
			return
		return

class stack():
	def __init__(self, api, addr,  port=None, loopback=False):
		self.loopback=loopback
		self.m = messages()
		self.client=networking.client()
		self.cm = manager(self.sender, api.interfaces,self.loopback)
		if port is None:
			self.port = random.randint(10000, 20000)
		else:
			self.port = port
		self.server=networking.server(addr,port,self._response(api,self.cm,self.sender,self.loopback).on_packet)
		threading.Thread(target=networking.start,args=()).start()

	def close(self):
		self.cm.close()
		self.client.close()
		self.server.close()
		return True

	def sender(self,mtype,cll):
		if mtype == 0:
			callid = str(uuid.uuid1()) #unique call id
			cll.set_id(callid)
			cll=self.cm.add_call(cll)
			
		for typ in cll.stream.get_types():
			packet = self.m.to_str(mtype, cll, typ)
			self.client.sendit(packet, cll.toaddr, cll.toport)
			logger.debug("Sent %s to %s:%s with %s", self.m.types[mtype], cll.toaddr, cll.toport, typ.name)

	class _response():
		def __init__(self, api, cm,sender ,loopback=False):
			self.loopback=loopback
			self.api = api
			self.sender = sender
			self.cm= cm
			self.m = messages()
			
		def on_packet(self,data,addr):
			if not data is None:
				#logger.debug("Client received message: %s", data)
				mtype, cll = self.m.from_str(data, self.cm)
				## if data is received
				if mtype == 4:
					if self.cm.is_call(cll.id):
						if self.loopback:
							cll=cll.reverse()
							self.sender(4,cll)
							logger.info("looping back data")
						else:
							for stype in cll.stream.get_types():
								cll.buffer.add(stype.name,stype.payload,stype.timestamp)
					dispatcher(self.api, "datareceived", [cll])
					return

				elif mtype == 0:
					## if connection requested
					if 1:
						cll = self.api._on_call_requested(cll)
						if len(cll.stream.get_types()) > 0:
							cll=self.cm.add_call(cll)
							cll=self.cm.activate_call(cll)
							dispatcher(self.api, "callestablished", [cll])
						else:
							self.cm.remove_call(cll)
							logger.info("Client declined call from: %s:%s ", cll.toaddr, cll.toport)
							dispatcher(self.api, "calldeclined", [cll.id])
						for typ in cll.stream.get_types(False):
							if typ.enabled:
								self.sender(1, cll)
								logger.info("Stream '%s' from %s:%d is accepted for call  %s and active", typ.name, cll.toaddr, cll.toport, cll.id)
								dispatcher(self.api, "streamaccepted", [cll.id, typ])
							else:
								self.sender(2, cll)
								logger.info("Stream '%s' from %s:%d is declined for  call %s ", typ.name, cll.toaddr, cll.toport, cll.id)
								dispatcher(self.api, "streamdeclined", [cll.id, typ])
					return
					
				elif mtype == 1:
				## if connection is accepted
					cll = self.cm.activate_call(cll)
					logger.info("Call '%s' from %s:%s is now authorized and active for %s", cll.id, cll.toaddr, cll.toport, cll.stream.get_types())
					dispatcher(self.api, "callestablished", [cll])
					return

				elif mtype == 2:
				## if connection is declined
					cll=self.cm.remove_call(cll.id)
					logger.info("Call '%s' to %s:%s declined for %s", cll.id, cll.toaddr, cll.toport, cll.stream.get_types())
					dispatcher(self.api, "calldeclined",[cll])
					return
